[Python]マルチプロセス処理だと処理時間が短くなる?[実験ノート]
皆さんこんにちは、クルトンです。
今回は簡単なプログラムを使って、マルチプロセスでプログラム実行した時の処理時間を計測してみました。
実行環境
Google Colab上で確認しました。
!python --version Python 3.7.15
処理するプログラムの内容
数値が入ったリストを用意して、リスト内の要素それぞれに掛け算をする、という簡単なプログラムを実行してみます。 コードは次のとおりです。
# 必要モジュールimport import time # 使用する配列と配列の総数を変数で準備 nums = [x for x in range(int(1e8))] nums_cnt=len(nums) def do_process(begin: int, end: int): start_time = time.perf_counter() # 開始時間 for i in range(begin, end): nums[i]*=3 end_time = time.perf_counter() # 終了時間 print("実行時間: ", end_time - start_time)
do_processメソッドは、プロセスを複数使う場合にリストのどこからどこまでの処理を担当するかを引数で受け取ります。 それでは実験していきましょう!
実験1: プロセス1つで確認する
マルチプロセスで確認する前に、まずは1つのプロセスで確認した場合の実行時間を計測します。
one_p = Process(target=do_process, args=(0,nums_cnt)) one_p.start() one_p.join()
Processの引数では、targetにメソッド名を、argsにtargetで指定したメソッドに渡す引数を記述します。
startメソッドで処理を実行し、joinメソッドを使って処理の終了を待ちます。
17.154019853999998秒で処理が終了しました!
実験2: プロセス2つで実行してみる
用意した配列を半分に区切って実行してみます。 今回実行に際しては、実行範囲2種類に対してそれぞれプロセスを割り当てて実行してみます。
# 2つで確認 two_p1=Process(target=do_process,args=(0,nums_cnt//2,)) two_p2=Process(target=do_process,args=(nums_cnt//2,nums_cnt)) two_p1.start() two_p2.start() two_p1.join() two_p2.join()
処理時間ですが、配列の前半部分(two_p1)が14.246526950000003秒
で、後半部分(two_p2)が14.233729984999997秒
でした。
つまり合計で28.480256935秒でした。
今回得られたそれぞれの実行時間と一つのプロセスのみで処理していた時を比較すると、処理時間が短くなっている事を確認できました。しかし、合計時間で比べるとが10秒以上遅くなってしまいました。
それぞれのプロセスでまとめてstartメソッドを実行してから、まとめてjoinメソッドで待つように書いてたのが原因かもしれないと考え、次のコードを実行しました。
# プロセス2つ(1つずつ実行)で確認 two_p1=Process(target=do_process,args=(0,nums_cnt//2,)) two_p2=Process(target=do_process,args=(nums_cnt//2,nums_cnt)) two_p1.start() two_p1.join() two_p2.start() two_p2.join()
プロセスの処理を1つずつ終了を待つようにすると、処理時間は配列前半部分が8.17154820399999秒
で、後半部分が7.996035918000004秒
となりました。
合計で16.167584121999994秒でした。
おそらく、メモリが占有されてしまうので限られたリソースでの処理になり、実行が遅くなっていたのが改善されたのだと思います。
1つのプロセスで実行するよりも複数のプロセスで処理する方が合計時間が短くなっていますね。ではさらにプロセス数を増やした場合はどうなるのか実験してみます。
実験3: プロセス4つで実行
さらにプロセスを増やして実行してみます。処理範囲を決めるインデックス取得のコードを先に書きます。
# 各プロセスに渡す範囲を決める配列を用意 process_num=4 # Processクラスを作る総数 process_range = nums_cnt//process_num # 範囲1つ分を決める process_sentinel = [] # 処理時に範囲を渡す時のindexを格納 for i in range(process_num): process_sentinel.append(i*process_range) process_sentinel.append(nums_cnt) print(process_sentinel)
インデックスを入れた配列を出力すると[0, 25000000, 50000000, 75000000, 100000000]
のようになっていると思います。
次に、複数のProcessクラスのインスタンスを今後も簡単に実験できるよう、動的に変数名をつけて用意してみます。
# プロセス変数をprocess_numの分だけ準備 index_cnt=0 for i in range(process_num): # 動的に準備 exec('p{}_{} = Process(target={},args={})'.format(process_num, i+1, 'do_process',(process_sentinel[index_cnt],process_sentinel[index_cnt+1],))) index_cnt+=1
これでp4_1, p4_2, p4_3, p4_4の4つの変数が作成できました。
では実行します。
# プロセスクラス4つで確認 for i in range(process_num): exec('p{}_{}.start()'.format(process_num,i+1)) exec('p{}_{}.join()'.format(process_num,i+1))
実行時間はそれぞれ4.083192964999995秒
, 4.038603355999996秒
, 4.07062053300001秒
, 4.019891293000001秒
となりました。
合計処理時間は16.212308147秒でした!
1つのプロセスで実行していた時と比べると少し早くなっていますね。
Poolクラスを使った場合の実装
複数処理をする場合にPoolクラスを使う事が可能です。
# プロセス4つで確認 from multiprocessing import Pool # starmapメソッドで使えるように修正 process_sentinel_tuple = [(process_sentinel[i],process_sentinel[i+1]) for i in range(len(process_sentinel)-1)] # 複数プロセスで実行 with Pool(processes=process_num) as pool: pool.starmap(do_process,process_sentinel_tuple)
実行時間はそれぞれ14.463421481000012秒
, 14.711410735000015秒
, 14.892837622999991秒
, 14.978986848000005秒
となりました。合計処理時間は59.046656687000024秒です。
環境にもよるとは思うのですが、どうやらProcessクラスを使った場合の方が速いのかもしれないです。
最後に8つのプロセスで実験してみます。
プロセス8つで確認
Processクラスを使って変数の準備から実行していきます。process_num変数の数値を4から8に変更し、変数を用意しstartメソッドとjoinメソッドを行うプログラムまで実行してみてください。
実行すると、それぞれの処理時間は2.105071817999999秒
, 2.0274987220000185秒
, 2.027574499000025秒
, 2.0310726009999485秒
, 2.029482388999952秒
, 2.0293496750000486秒
, 2.0422307499999874秒
, 2.0729569960000163秒
でした。
合計処理時間は16.365237449999995秒でした!
プロセスクラスでの処理する個数を増やせば増やすほど、処理時間が短くなるという単純な関係でないものの、マルチプロセスであれば単純な処理であっても処理時間が短くできました!
終わりに
今回はマルチプロセスで処理する時の処理時間の変化を確認してみました。
配列内の数値全てに掛け算をするという簡単な処理であっても短くできました。ただし、CPUの状況によって処理時間が毎回一定で終わるわけではないのでそこは注意が必要です。(概ねマルチプロセスの方が処理時間が少しだけ短くなりました。)
今回はここまで。
それでは、また!